NOTE

2.2 Redis RateLimiter

1. 是什么 - 基于Redis实现的限流 2. 计数器算法 2.1. 接口 2.1.1. 访问次数加一 - 参数:key - 返回: - 当前访问次数 - 访问次数重置的剩余时长 2.1.2. 获取当前访问次数 - 参数:key - 返回: - 当前访问次数 - 访问次数重置的剩余时长 2.2.

Redis / Cache创建于 更新于 historical

这是历史学习笔记,可能存在过时或不完整的理解。

1. 是什么

  • 基于Redis实现的限流

2. 计数器算法

2.1. 接口

2.1.1. 访问次数加一

  • 参数:key
  • 返回:
    • 当前访问次数
    • 访问次数重置的剩余时长

2.1.2. 获取当前访问次数

  • 参数:key
  • 返回:
    • 当前访问次数
    • 访问次数重置的剩余时长

2.2. 逻辑

  • 访问次数加一

    local freqKey = KEYS[1]
    local freqTTL = tonumber(ARGV[1])
    
    local function isFreqKeyExists()
      return redis.call("EXISTS", freqKey)
    end
    
    local function incrFreqCount()
        return redis.call("INCR", freqKey)
    end
    
    local function getFreqTTL()
        return redis.call("TTL", freqKey)
    end
    
    local function initFreqKey()
        return redis.call("SET", freqKey, 1, "EX", freqTTL)
    end
    
    local function main()
        if isFreqKeyExists() == 1 then
          local freqCount = incrFreqCount()
          local freqTTL = getFreqTTL()
          return {freqCount, freqTTL}
        else
          initFreqKey()
          return {1, freqTTL}
        end
    end
    
    return main()
  • 获取当前访问次数

    local freqKey = KEYS[1]
    
    local function getFreqCount()
        return redis.call("GET", freqKey)
    end
    
    local function getFreqTTL()
        return redis.call("TTL", freqKey)
    end
    
    local function main()
        local freqCount = getFreqCount()
        if not freqCount then
          return {0, 0}
        else 
          local freqTTL = getFreqTTL()
          return {freqCount, freqTTL}
        end
    end
    
    return main()

2.2.1. 为什么需要使用Lua脚本封装

  • 多条命令封装到一起避免多次往返服务器以提升效率
    • 当然这个用pipeline也可以实现
  • 防止并发问题
    • 比如以下的场景就会让业务方很迷惑,明明访问次数为0,为什么下次重置的时间却是60s
    1. 获取当前访问次数:get得到访问次数为0
    2. 访问次数加一:incr次数为1
    3. 访问次数加一:expire超时时间为60s
    4. 获取当前访问次数:pttl返回为60s
-- appid_业务key_当前时间类型下的当前时间
local freqKey = KEYS[1]
-- 逻辑过期时间戳:1651420800
local freqExpireTs = tonumber(ARGV[1])
-- 实际过期时间戳:ttl的基础上+7天:ttl+604800
local freqTTL = tonumber(ARGV[2])
-- 当前时间戳
local currentTimeTs = tonumber(ARGV[3])

local function isFreqKeyExists(freqKey)
  local isExists = redis.call("EXISTS", freqKey)
  return isExists == 1
end

local function getFreqExpireTs()
  return tonumber(redis.call("HGET", freqKey, "expire"))
end

local function incrFreqCount()
    return redis.call("HINCRBY", freqKey, "count", 1)
end

local function initFreqKey(freqKey, freqExpireTs, freqTTL)
    redis.call("HMSET", freqKey, "count", 1, "expire", freqExpireTs)
    redis.call("EXPIRE", freqKey, freqTTL)
end

local function isFreqExpired(freqExpireTs, currentTimeTs)
    return currentTimeTs >= freqExpireTs
end

local function main()
    if isFreqKeyExists(freqKey) then
        local freqExpireTs = getFreqExpireTs(freqKey)
        if isFreqExpired(freqExpireTs, currentTimeTs)==false then
            local freqCount = incrFreqCount()
            return {freqCount, freqExpireTs}
        end
    end
    initFreqKey(freqKey, freqExpireTs, freqTTL)
    return {1, freqExpireTs}
end

return main()
local freqKey = KEYS[1]
local currentTimeTs = tonumber(ARGV[1])

local function isFreqKeyExists(freqKey)
  local isExists = redis.call("EXISTS", freqKey)
  return isExists == 1
end

local function getFreq(freqKey)
    local freq = redis.call("HMGET", freqKey, "count", "expire")
    return tonumber(freq[1]), tonumber(freq[2])
end

local function isFreqAlived(freqExpireTs, currentTimeTs)
    return currentTimeTs < freqExpireTs
end

local function main()
    if isFreqKeyExists(freqKey) then
        local freqCount, freqExpireTs = getFreq(freqKey)
        if isFreqAlived(freqExpireTs, currentTimeTs) then
            return {freqCount, freqExpireTs}
        end
    end
    return {0, currentTimeTs}
end

return main()

3. 参考